/**
 * @license
 * Copyright 2021 Du Tian Wei
 * SPDX-License-Identifier: Apache-2.0
 */
"use strict"
import * as util from './util.mjs'
import * as obvm from './vm.mjs'

function handleCode(code) {
    return code.split('\n').filter(line =>
        (!line.trim().startsWith('import'))
        &&
        (!line.trim().startsWith('export'))
        &&
        (!line.trim().startsWith('"use strict"'))
    ).join('\n');
}
/**
 * debugger连接器，实际应用场景应该实现子类
 */
class DebuggerBroker {
    onEvent(eventName, block, args, level, stack) { }
}
class Debugger {
    /**
     * 
     * @param {connector:DebuggerConnector} options 
     */
    constructor(options) {
        this.options = options || {};
        this.instructionCount = 0;
        this.logFilter = false;
    }
    static pausableInstructionWrap(instruction) {
        return function (st) {
            if (st.fsm.VM.pausing) {
                st.fsm.VM.pausing = false;
                throw new obvm.VMPausedException();
            }
            return instruction.apply(null, arguments)
        };
    }
    instructionWrap(instruction) {
        if (this.options.instructionWrap) {
            instruction = this.options.instructionWrap(instruction);
        }
        let that = this;
        return function () {
            that.instructionCount++;
            // console.log(that.instructionCount);
            return instruction.apply(null, arguments);
        };
    }
    registerWrap(register) {
        if (this.options.registerWrap) {
            register = this.options.registerWrap(register);
        }
        let that = this;
        return function () {
            that.instructionCount++;
            let ret = register.apply(null, arguments);
            return ret;
        };
    }
    invokeStack = [];
    setCurrentBlockEnter(block, st, uf, locals, pos) {
        let top = this.invokeStack[0];
        if (top && top.block.BlockId == block.BlockId) {
            console.log('skip enter', block);
            return;
        }
        console.log('enter', block);
        this.invokeStack.unshift({
            block: block,
            st: st,
            uf: uf,
            locals: locals,
            pos: pos,
        });
        this.currentBlockId = block.BlockId;
        this.registerType = -1;
        this.lastResult = null;
    }
    setCurrentBlockExit(block, st, uf, locals, pos) {
        console.log('exit', block);
        let record = this.invokeStack.shift();
        console.log(record);
        this.printStack()
        this.currentBlockId = block.BlockId;
        this.registerType = -1;
        this.lastResult = null;
    }
    printStack() {
        console.log('stack\n' + this.invokeStack.map(item => {
            return item.block.BlockId;
        }).join('\n'));
    }
    setLastResult(registerType, result) {
        this.registerType = registerType;
        this.lastResult = result;
    }
    debugEvent(eventName, block, args, level, stack) {
        if (this.options.broker) {
            if (typeof (level) != 'number') {
                console.log('level is not a number ' + level);
                level = 3
            }
            this.options.broker.onEvent(eventName, block, args, level, this.stackMsg(stack));
        }
    }
    stackMsg(stack) {
        // if (stack) {
        //     return stack.map(stackItem => {

        //     });
        // }
    }
}
function getAllKeys(obj) {
    let keys = [];

    for (let proto = obj; proto !== null; proto = Object.getPrototypeOf(proto)) {
        keys = keys.concat(Reflect.ownKeys(proto));
    }

    return [...new Set(keys)]; // 使用Set去除重复属性，然后转换回数组
}

class NativeObjectAgent {
    interpreter; nativeConstructor;
    constructor(interpreter) {
        this.interpreter = interpreter;
    }
    wrapNativeObject(pseudoThis, nativeObj) {
        if (pseudoThis.target) {
            return;
        }
        let that = this;
        pseudoThis.target = nativeObj;
        nativeObj.__pseudo = pseudoThis;
        this.interpreter.setProperty(pseudoThis, '__debugger', 'roarlang');
        let set = this.interpreter.createNativeFunction((index, v) => {
            v = that.pseudoArgsToNative(v);
            pseudoThis.target[index] = v;
        }, false);
        this.interpreter.setProperty(pseudoThis, '__set', set);
        let get = this.interpreter.createNativeFunction((index) => {
            let v = pseudoThis.target[index];
            let p = that.nativeToPseudo(v);
            return p;
        }, false);
        this.interpreter.setProperty(pseudoThis, '__get', get);
        let propList = this.interpreter.createNativeFunction(() => {
            var pseudoArray = that.interpreter.createArray();
            var props = getAllKeys(pseudoThis.target);
            for (var i = 0; i < props.length; i++) {
                that.interpreter.setProperty(pseudoArray, i, props[i]);
            }
            return pseudoArray;
        }, false);
        this.interpreter.setProperty(pseudoThis, '__propList', propList);
    }
    nativeToPseudo(v) {
        if (v == null) {
            return v;
        }
        let that = this;
        if (v.__pseudo) {
            return v.__pseudo;
        }
        if (!(v instanceof Interpreter.Object)) {
            if (typeof v == 'function') {
                var pv = that.interpreter.createNativeFunction(function (...var_args) {
                    let args = that.pseudoArgsToNative(var_args);
                    let self = typeof (this) == 'undefined' ? null : this.target;
                    let r = v.apply(self, args);
                    let p = that.nativeToPseudo(r);
                    if (typeof r == 'object') {
                        that.wrapNativeObject(p, r);
                    } else if (typeof r == 'function') {
                        p.target = r;
                        r.__pseudo = p;
                    }
                    return p;
                });
                pv.target = v;
                return pv;
            } else if (Array.isArray(v)) {
                let arr = v.map(item => (that.nativeToPseudo(item)));
                let pv = that.interpreter.arrayNativeToPseudo(arr);
                return pv;
            } else if (typeof v == 'object') {
                let _constructor = null;
                if (!v.constructor || (v.constructor !== Object)) {
                    _constructor = that.interpreter.getGlobalScope().object.properties[v.constructor.name];
                    if (!_constructor) {
                        throw new Error('constructor not found:' + v.constructor.name);
                    }
                }
                var pv = that.interpreter.createObject(_constructor);
                this.wrapNativeObject(pv, v);
                return pv;
            }
        }
        return v;
    }
    pseudoArgsToNative(arg) {
        if (!Array.isArray(arg)) {
            if (arg instanceof Interpreter.Object) {
                if (arg.target) {
                    return arg.target;
                }
                return this.interpreter.pseudoToNative(arg);
            }
            return arg;
        }
        return arg.map(a => this.pseudoArgsToNative(a));
    }
    buildConstructor() {
        let that = this;
        let _agentConstructor = function (var_args) {
            let args = that.interpreter.arrayPseudoToNative(var_args);
            let agent = args.shift();
            args = that.pseudoArgsToNative(args);
            let nativeObj = new window[agent](...args);
            that.wrapNativeObject(this, nativeObj);
            return this;
        };
        let agentConstructor = this.interpreter.createNativeFunction(_agentConstructor, true);
        return agentConstructor;
    }
}
class DebuggerableVM extends EventTarget {
    static vm_mjs;
    interpreter;
    constructor(scriptArrayBuffer, config, nativeLibs, nativeClassNames) {
        super();
        this.config = config;
        this.nativeLibs = nativeLibs;
        this.scriptArrayBuffer = scriptArrayBuffer;
        this.nativeClassNames = nativeClassNames;
    }
    InterpreterInit(interpreter, globalObject) {
        let agent = new NativeObjectAgent(interpreter);
        interpreter.setProperty(globalObject, 'NativeObjectAgent', agent.buildConstructor());
        // console
        var console_api = agent.nativeToPseudo(console);
        interpreter.setProperty(globalObject, 'console', console_api);

        // util
        var util_api = agent.nativeToPseudo(Object.assign({}, util));
        interpreter.setProperty(globalObject, 'util', util_api);

        // native libs
        var nativeLibs = agent.nativeToPseudo(this.nativeLibs);
        interpreter.setProperty(globalObject, 'nativeLibs', nativeLibs);
    }
    static buildAgentClass(classnames) {
        return classnames.map(name => `
class ${name} extends PseudoNativeObjectAgent {
    constructor(var_args) {
        super(['${name}', ...arguments]);
    }
}
        `).join('\n');
    }

    async CreateFSM() {
        if (!DebuggerableVM.vm_mjs) {
            let debuggerClient = await (await fetch('../runtime/debuggerClient.mjs')).text();
            debuggerClient += DebuggerableVM.buildAgentClass(this.nativeClassNames);
            let code = await (await fetch('../runtime/vm.mjs')).text();
            let libs = await Promise.all(this.nativeLibs.map(async (lib) => '//' + lib + '\n' + await (await fetch(lib)).text()));
            code += libs.join('\n');
            code = debuggerClient + '\n' + code;
            code = handleCode(code);
            // for (let path of this.nativeLibs) {
            //     let lib = await (await fetch(path)).text();
            //     code += '\n' + handleCode(lib);
            // }
            var options = {
                'presets': ['es2015']
            };

            let base64 = util.arrayBufferToBase64(this.scriptArrayBuffer);
            let loadScript = 'loadScript("' + base64 + '");'
            let fullcode = code + '\n' + '\n' + loadScript;
            let b_code = Babel.transform(fullcode, options).code;
            DebuggerableVM.vm_mjs = b_code;
            this.interpreter = new Interpreter(DebuggerableVM.vm_mjs, this.InterpreterInit.bind(this));
            let i = setInterval(() => {
                try {
                    this.interpreter.run();
                } catch (e) {
                    console.log(e);
                    clearInterval(i);
                    let stack = this.interpreter.getStateStack();
                    console.log(stack);
                    stack.map((s, i) => { console.log(i + ') ' + s.node.loc.start.line + ':' + s.node.loc.start.column); })
                    debugger
                }
            }, 0);
        }
    }


    isRunning() {
        return true;
    }
    update() { }
}
export {
    Debugger, DebuggerBroker, DebuggerableVM
}
